import dateutil.parser
import six
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from conf.config import scheduler_config


def fix_job_def(job_def):
    """
    Replaces the datetime in string by datetime object.
    """
    if six.PY2 and isinstance(job_def.get('func'), six.text_type):
        # when a job comes from the endpoint, strings are unicode
        # because that's how json package deserialize the bytes.
        # we had a case where APScheduler failed to import the func based
        # on its name because Py2 expected a str and not unicode on __import__().
        # it happened only for a user, I wasn't able to determine why that occurred for him,
        # a workaround is to convert the func to str.

        # full story: https://github.com/viniciuschiele/flask-apscheduler/issues/75

        job_def['func'] = str(job_def.get('func'))

    if isinstance(job_def.get('start_date'), six.string_types):
        job_def['start_date'] = dateutil.parser.parse(job_def.get('start_date'))

    if isinstance(job_def.get('end_date'), six.string_types):
        job_def['end_date'] = dateutil.parser.parse(job_def.get('end_date'))

    if isinstance(job_def.get('run_date'), six.string_types):
        job_def['run_date'] = dateutil.parser.parse(job_def.get('run_date'))

    # it keeps compatibility backward
    if isinstance(job_def.get('trigger'), dict):
        trigger = job_def.pop('trigger')
        job_def['trigger'] = trigger.pop('type', 'date')
        job_def.update(trigger)


class APScheduler(object):

    def __init__(self, obj=None):
        self._scheduler = obj or AsyncIOScheduler()
        self.config = scheduler_config.SCHEDULER_TASK_CONFIG

    def init(self):
        self._load_config()
        self._load_jobs()

    def add_job(self, id, func, **kwargs):
        """
        Add the given job to the job list and wakes up the scheduler if it's already running.

        :param str id: explicit identifier for the job (for modifying it later)
        :param func: callable (or a textual reference to one) to run at the given time
        """

        job_def = dict(kwargs)
        job_def['id'] = id
        job_def['func'] = func
        job_def['name'] = job_def.get('name') or id

        fix_job_def(job_def)

        return self._scheduler.add_job(**job_def)

    def remove_job(self, job_id, jobstore=None):
        return self._scheduler.remove_job(job_id, jobstore)

    def get_job(self, id, jobstore=None):
        """
        Return the Job that matches the given ``id``.

        :param str id: the identifier of the job
        :param str jobstore: alias of the job store that most likely contains the job
        :return: the Job by the given ID, or ``None`` if it wasn't found
        :rtype: Job
        """

        return self._scheduler.get_job(id, jobstore)

    def _load_jobs(self):
        """
        Load the job definitions from the Flask configuration.
        """
        jobs = self.config.get('JOBS')

        if jobs:
            for job in jobs:
                self.add_job(**job)

    def _load_config(self):
        """
        Load the configuration from the Flask configuration.
        """
        options = dict()
        job_stores = self.config.get('SCHEDULER_JOBSTORES')

        if job_stores:
            options['jobstores'] = job_stores

        executors = self.config.get('SCHEDULER_EXECUTORS')
        if executors:
            options['executors'] = executors

        job_defaults = self.config.get('SCHEDULER_JOB_DEFAULTS')
        if job_defaults:
            options['job_defaults'] = job_defaults

        timezone = self.config.get('SCHEDULER_TIMEZONE')
        if timezone:
            options['timezone'] = timezone

        self._scheduler.configure(**options)

    def start(self, paused=False):
        """
        Start the scheduler.
        :param bool paused: if True, don't start job processing until resume is called.
        """
        self._scheduler.start(paused=paused)

    def shutdown(self, wait=True):
        """
        Shut down the scheduler. Does not interrupt any currently running jobs.

        :param bool wait: ``True`` to wait until all currently executing jobs have finished
        :raises SchedulerNotRunningError: if the scheduler has not been started yet
        """
        self._scheduler.shutdown(wait)


scheduler = APScheduler()
